/////////////////////////////////////////////////////////////////////////////
// Name:        tts.cpp
// Purpose:     Text to speech classes for wxWidgets
// Author:      Julian Smart
// Modified by: 
// Created:     2009-02-10
// RCS-ID:      
// Copyright:   (c) Julian Smart
// Licence:     New BSD License
/////////////////////////////////////////////////////////////////////////////

#include "wx/wx.h"

#include "wx/filename.h"
#include "wx/textfile.h"
#include "wx/txtstrm.h"
#include "wx/datstrm.h"
#include "wx/wfstream.h"
#include "wx/dir.h"
#include "wx/html/htmlpars.h"

#include "tts/tts.h"
#include "tts/tts_htmlparser.h"

/*
 * wxTextToSpeech implementation
 */

IMPLEMENT_DYNAMIC_CLASS( wxTextToSpeech, wxEvtHandler )

wxTextToSpeech::wxTextToSpeech()
{
    Init();
}

wxTextToSpeech::~wxTextToSpeech()
{
    ClearHandlers();
}

// Member initialisation
void wxTextToSpeech::Init()
{
    m_errorCode = 0;
    m_currentHandler = NULL;
    m_transformer = NULL;
}

// Speak the text
bool wxTextToSpeech::Speak(const wxString& text, int options)
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->Speak(text, options);
    else
        return false;
}

// Speak the file
bool wxTextToSpeech::SpeakFile(const wxString& filename, int options, const wxString& waveFilename)
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->SpeakFile(filename, options, waveFilename);
    else
        return false;
}

// Is it speaking?
bool wxTextToSpeech::IsSpeaking() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->IsSpeaking();
    else
        return false;
}

// Stop speaking
bool wxTextToSpeech::Stop()
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->Stop();
    else
        return false;
}

// Pause
bool wxTextToSpeech::Pause()
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->Pause();
    else
        return false;
}

// Pause
bool wxTextToSpeech::Resume()
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->Resume();
    else
        return false;
}

// Can we pause?
bool wxTextToSpeech::CanPause() const
{
    return IsSpeaking() && !IsPaused() && HasCapability(wxTTS_CAP_PAUSE);
}

// Is it paused?
bool wxTextToSpeech::IsPaused() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->IsPaused();
    else
        return false;
}

// Skip forward or backward the given number of items. A positive value skips forward,
// and a negative value skips back. A value of zero skips to the start of the item.
// itemType may be ignored by some or all engines.
bool wxTextToSpeech::Skip(int toSkip, wxTTSSkipType itemType)
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->Skip(toSkip, itemType);
    else
        return false;
}

// Can we skip right now?
bool wxTextToSpeech::CanSkip() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->CanSkip();
    else
        return false;
}

// Set a property
void wxTextToSpeech::SetProperty(const wxTTSProperty& property)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetProperty(property);
}

void wxTextToSpeech::SetProperty(const wxString& name, const wxString& value)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetProperty(name, value);
}

void wxTextToSpeech::SetProperty(const wxString& name, const wxString& value, const wxArrayString& choices)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetProperty(name, value, choices);
}

void wxTextToSpeech::SetProperty(const wxString& name, long value)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetProperty(name, value);
}

void wxTextToSpeech::SetProperty(const wxString& name, int value)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetProperty(name, value);
}

void wxTextToSpeech::SetProperty(const wxString& name, bool value)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetProperty(name, value);
}

void wxTextToSpeech::SetProperty(const wxVariant& value)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetProperty(value);
}

// Get a property
wxTTSProperty wxTextToSpeech::GetProperty(const wxString& name) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetProperty(name);
    else
        return wxTTSProperty();
}

// Get a property value variant
wxVariant wxTextToSpeech::GetPropertyValue(const wxString& name) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetPropertyValue(name);
    else
        return wxVariant();
}

// Get a string property value
wxString wxTextToSpeech::GetPropertyString(const wxString& name) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetPropertyString(name);
    else
        return wxEmptyString;
}

// Get a long property value
long wxTextToSpeech::GetPropertyLong(const wxString& name) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetPropertyLong(name);
    else
        return 0;
}

// Get a double property value
double wxTextToSpeech::GetPropertyDouble(const wxString& name) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetPropertyDouble(name);
    else
        return 0.0;
}

// Get a bool property value
long wxTextToSpeech::GetPropertyBool(const wxString& name) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetPropertyBool(name);
    else
        return false;
}

// Does this property exist?
bool wxTextToSpeech::HasProperty(const wxString& name) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->HasProperty(name);
    else
        return false;
}

// Get current properties - doesn't necessarily mean they're all supported
wxArrayString wxTextToSpeech::EnumerateProperties() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->EnumerateProperties();
    else
        return wxArrayString();
}

// Does this handler have the specified capability?
bool wxTextToSpeech::HasCapability(wxTTSCapability capability) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->HasCapability(capability);
    else
        return false;
}

// Can these two voices be used together? Pass an empty string to the voice parameters
// to find out whether the engine in general supports simultaneous voices. Pass voice names
// to find out if these voices can be used simultaneously, when using the given engine.
// The voice names can be the same.
bool wxTextToSpeech::VoicesAreCompatible(const wxString& voice1, const wxString& voice2) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->VoicesAreCompatible(voice1, voice2);
    else
        return false;
}

bool wxTextToSpeech::VoicesAreCompatible(const wxString& engine, const wxString& voice1, const wxString& voice2) const
{
    /// Find handler by id
    wxTTSHandler* handler = FindHandler(engine);
    if (handler)
        return handler->VoicesAreCompatible(voice1, voice2);
    else
        return false;
}

// Get property count
int wxTextToSpeech::GetPropertyCount() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetPropertyCount();
    else
        return 0;
}

// Get property
wxTTSProperty wxTextToSpeech::GetProperty(size_t i) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetProperty(i);
    else
        return wxTTSProperty();
}

// Set/get the program location
void wxTextToSpeech::SetProgramLocation(const wxString& location)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetProgramLocation(location);
}

wxString wxTextToSpeech::GetProgramLocation() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetProgramLocation();
    else
        return wxEmptyString;
}

// Set/get the program location
void wxTextToSpeech::SetDataLocation(const wxString& location)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetDataLocation(location);
}

wxString wxTextToSpeech::GetDataLocation() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetDataLocation();
    else
        return wxEmptyString;
}

// Set/get the voice
void wxTextToSpeech::SetVoice(const wxString& voice)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetVoice(voice);
}

wxString wxTextToSpeech::GetVoice() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetVoice();
    else
        return wxEmptyString;
}

// Get the default voice
wxString wxTextToSpeech::GetDefaultVoice() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetDefaultVoice();
    else
        return wxEmptyString;
}


// Set/get the voice variant
void wxTextToSpeech::SetVoiceVariant(const wxString& variant)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetVoiceVariant(variant);
}

wxString wxTextToSpeech::GetVoiceVariant() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetVoiceVariant();
    else
        return wxEmptyString;
}

// Get the default voice variant
wxString wxTextToSpeech::GetDefaultVoiceVariant() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetDefaultVoiceVariant();
    else
        return wxEmptyString;
}

// Set/get the speed
void wxTextToSpeech::SetSpeed(int speed, wxTTSQuantityType quantity)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetSpeed(speed, quantity);
}

int wxTextToSpeech::GetSpeed(wxTTSQuantityType quantity) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetSpeed(quantity);
    else
        return 0;
}

// Set/get the volume
void wxTextToSpeech::SetVolume(int vol, wxTTSQuantityType quantity)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetVolume(vol, quantity);
}

int wxTextToSpeech::GetVolume(wxTTSQuantityType quantity) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetVolume(quantity);
    else
        return 0;
}

// Set/get the pitch
void wxTextToSpeech::SetPitch(int pitch, wxTTSQuantityType quantity)
{
    if (GetCurrentHandler())
        GetCurrentHandler()->SetPitch(pitch, quantity);
}

int wxTextToSpeech::GetPitch(wxTTSQuantityType quantity) const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetPitch(quantity);
    else
        return 0;
}

// Set the priority. 0 (wxTTS_Normal) is normal priority, 1 (wxTTS_Alert) is alert priority,
// and 2 (wxTTS_Over) is over priority, which mixes audio with other audio on the system.
// Using wxTTS_Over can cause crashes for engines that are not coded for re-entrancy.
void wxTextToSpeech::SetPriority(int priority)
{
    SetProperty(wxTTS_PROP_PRIORITY, priority);
}

// Get the priority
int wxTextToSpeech::GetPriority() const
{
    return GetPropertyLong(wxTTS_PROP_PRIORITY);
}


// Get a list of the available voices
wxArrayString wxTextToSpeech::GetAvailableVoices() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetAvailableVoices();
    else
        return wxArrayString();
}

// Get a list of the available voice variants
wxArrayString wxTextToSpeech::GetAvailableVoiceVariants() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetAvailableVoiceVariants();
    else
        return wxArrayString();
}

// Get an array of engine id names
wxArrayString wxTextToSpeech::GetEngineIds() const
{
    wxArrayString engineIds;
    size_t i;
    for (i = 0; i < (size_t) GetHandlerCount(); i++)
    {
        wxTTSHandler* handler = GetHandler(i);
        engineIds.Add(handler->GetId());
    }
    return engineIds;
}

// Get an array of engine display names
wxArrayString wxTextToSpeech::GetEngineDisplayNames() const
{
    wxArrayString displayNames;
    size_t i;
    for (i = 0; i < (size_t) GetHandlerCount(); i++)
    {
        wxTTSHandler* handler = GetHandler(i);
        displayNames.Add(handler->GetDisplayName());
    }
    return displayNames;
}

// Get a list of the available engine descriptions
wxArrayString wxTextToSpeech::GetEngineDescriptions() const
{
    wxArrayString descriptions;
    size_t i;
    for (i = 0; i < (size_t) GetHandlerCount(); i++)
    {
        wxTTSHandler* handler = GetHandler(i);
        descriptions.Add(handler->GetDescription());
    }
    return descriptions;
}

// Add handler
bool wxTextToSpeech::AddHandler(wxTTSHandler* handler)
{
    if (!FindHandler(handler->GetId()))
    {
        m_handlers.Append(handler);
        handler->SetTextToSpeechObject(this);
        return true;
    }
    else
        return false;
}

// Get handler
wxTTSHandler* wxTextToSpeech::GetHandler(size_t i) const
{
    return (wxTTSHandler*) m_handlers.Item(i)->GetData();
}

// Find handler by id
wxTTSHandler* wxTextToSpeech::FindHandler(const wxString& name) const
{
    size_t i;
    for (i = 0; i < (size_t) GetHandlerCount(); i++)
    {
        wxTTSHandler* handler = GetHandler(i);
        if (handler->GetId() == name)
            return handler;
    }
    return NULL;
}

// Remove all handlers, uninitializing each one
void wxTextToSpeech::ClearHandlers()
{
    size_t i;
    for (i = 0; i < (size_t) GetHandlerCount(); i++)
    {
        wxTTSHandler* handler = GetHandler(i);
        handler->Uninitialize();
    }
    m_handlers.DeleteContents(true);
    m_handlers.Clear();
    m_handlers.DeleteContents(false);
}

// Set current engine by id
bool wxTextToSpeech::SetEngine(const wxString& id)
{
    wxTTSHandler* handler = FindHandler(id);
    if (handler)
    {
        // TODO: uninitialize previous handlers?
        if (!handler->Initialize())
        {
            wxLogError(wxString::Format(_("Sorry, could not initialize %s."), id.c_str()));
            return false;
        }

        SetCurrentHandler(handler);
        return true;
    }
    else
        return false;
}

// Get current engine by id
wxString wxTextToSpeech::GetEngine() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetId();
    else
        return wxEmptyString;
}

// For when the handler is using an external command,
// deal with input from command
bool wxTextToSpeech::ProcessInput()
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->ProcessInput();
    else
        return true;
}

// Get last command, if using external command
wxString wxTextToSpeech::GetLastCommand() const
{
    if (GetCurrentHandler())
        return GetCurrentHandler()->GetLastCommand();
    else
        return wxEmptyString;
}

// Append paths without duplicating separator
wxString wxTextToSpeech::AppendPaths(const wxString& p1, const wxString& p2, const wxString& sep)
{
    wxString path = p1;

    if (p1.Last() != wxT('\\') && p1.Last() != wxT('/') && p2[0] != wxT('\\') && p2[0] != wxT('/'))
        path += sep;

    path += p2;

    return path;
}

// Intelligent strip extension: don't strip before a separator,
// and don't strip if the 'extension' is different from the passed extension
// that we expect - can be comma-separated list.
bool wxTextToSpeech::StripExtension(wxString& path, const wxString& validExtensions)
{
    wxString p, name, ext;
    wxSplitPath(path, &p, &name, &ext);

    if (!validExtensions.IsEmpty() && !ext.IsEmpty() && validExtensions.Find(ext) == wxNOT_FOUND)
        return false;
    else if (!ext.IsEmpty())
    {
        path = path.Left(path.Length() - ext.Length() - 1);
        return true;
    }
    else
        return false;
}

// Find the encoding of an HTML file
wxString wxTextToSpeech::FindHtmlEncoding(const wxString& filename)
{
    wxFFile file(filename);
    if (file.IsOpened())
    {
        const int len = 400;
        char buf[len+1];
        buf[file.Read(buf, len)] = 0;

        wxString str = wxString::FromAscii(buf);
        wxString encoding;
        wxString toFind(wxT("charset="));

        int pos = str.Find(toFind);
        if (pos != -1)
        {
            size_t i = pos + toFind.Length();
            while (i < str.Length() && str[i] != wxT('"') && str[i] != wxT(' ') && str[i] != wxT(';'))
            {
                encoding += str[i];
                i ++;
            }
            return encoding;
        }

    }
    return wxEmptyString;
}

// Find the encoding of an XML file
wxString wxTextToSpeech::FindXmlEncoding(const wxString& filename)
{
    wxFFile file(filename);
    if (file.IsOpened())
    {
        const int len = 400;
        char buf[len+1];
        buf[file.Read(buf, len)] = 0;

        wxString str = wxString::FromAscii(buf);
        wxString encoding;
        wxString toFind(wxT("encoding="));

        int pos = str.Find(toFind);
        if (pos != -1)
        {
            size_t i = pos + toFind.Length()+1;
            while (i < str.Length() && str[i] != wxT('"') && str[i] != wxT(' '))
            {
                encoding += str[i];
                i ++;
            }
            return encoding;
        }

    }
    return wxEmptyString;
}

// Is this an XML file?
bool wxTextToSpeech::IsXmlFile(const wxString& filename)
{
    wxFFile file(filename);
    if (file.IsOpened())
    {
        const int len = 400;
        char buf[len+1];
        buf[file.Read(buf, len)] = 0;

        wxString str = wxString::FromAscii(buf);
        return str.Find(wxT("<?xml")) != wxNOT_FOUND;
    }
    else
        return false;
}

// Is this an XML string?
bool wxTextToSpeech::IsXmlString(const wxString& text)
{
    return text.Find(wxT("<?xml")) != wxNOT_FOUND;
}


/*
 * wxTTSPronounciationTable implementation
 */

wxTTSTransformer::wxTTSTransformer()
{
    m_dirty = true;
    m_modified = false;
}

wxTTSTransformer::~wxTTSTransformer()
{
}

// Add a string
void wxTTSTransformer::AddString(const wxString& word, const wxString& replacement)
{
    m_strings.Add(word.Lower());
    m_replacements.Add(replacement);
    m_dirty = true;
    m_modified = true;
}

// Insert string before this one
void wxTTSTransformer::InsertString(const wxString& word, const wxString& replacement, int pos)
{
    m_strings.Insert(word.Lower(), pos);
    m_replacements.Insert(replacement, pos);
    m_dirty = true;
    m_modified = true;
}

// Remove a string
void wxTTSTransformer::RemoveString(const wxString& word)
{
    int idx = m_strings.Index(word.Lower());
    if (idx != wxNOT_FOUND)
    {
        m_strings.RemoveAt(idx);
        m_replacements.RemoveAt(idx);

        m_dirty = true;
        m_modified = true;
    }
}

void wxTTSTransformer::RemoveString(size_t i)
{
    wxASSERT(i < m_strings.GetCount());
    m_strings.RemoveAt(i);
    m_dirty = true;
    m_modified = true;
}

// Get a string
wxString wxTTSTransformer::GetString(const wxString& word)
{
    if (m_dirty)
    {
        int idx = m_strings.Index(word.Lower());
        if (idx != wxNOT_FOUND)
        {
            return m_replacements[idx];
        }
        else
            return wxEmptyString;
    }
    else
    {
        wxTTSStringHashMap::iterator it = m_hashMap.find(word.Lower());
        if (it != m_hashMap.end())
            return * (it->second);
        else
            return wxEmptyString;
    }
}

// Has string
bool wxTTSTransformer::HasString(const wxString& word)
{
    if (m_dirty)
    {
        int idx = m_strings.Index(word.Lower());
        return (idx != wxNOT_FOUND);
    }
    else
    {
        wxTTSStringHashMap::iterator it = m_hashMap.find(word.Lower());
        return (it != m_hashMap.end());
    }
}

// Clear
void wxTTSTransformer::Clear()
{
    m_strings.Clear();
    m_replacements.Clear();
    m_hashMap.clear();
    m_dirty = true;
    m_modified = true;
}

// Index the strings
void wxTTSTransformer::Index()
{
    if (!m_dirty)
        return;

    m_hashMap.clear();
    size_t i;
    for (i = 0; i < m_strings.GetCount(); i++)
    {
        m_hashMap[m_strings[i]] = & (m_replacements[i]);
    }

    m_dirty = false;
}

static inline bool wxIsWhitespace(const wxChar& ch)
{
    return (ch == wxT(' ') || ch == wxT('\n') || ch == wxT('\r') || ch == wxT('\t'));
}

static inline bool wxIsDelimiter(const wxChar& ch)
{
    return (wxIsWhitespace(ch) ||
           !wxIsalnum(ch)
           );
}

// Replace strings in a text string
bool wxTTSTransformer::TransformString(const wxString& input, wxString& output, int WXUNUSED(options))
{
    wxString text(input);
    int count = 0;
    size_t i = 0;
    int len = text.Length();
    while (i < (size_t) len)
    {
        while (i < (size_t) len && wxIsDelimiter(text[i]))
            i ++;
        if (i < (size_t) len)
        {
            int last = i;
            wxString word;
            while (i < (size_t) len && !wxIsDelimiter(text[i]))
            {
                word += text[i];
                i ++;
            }

            if (HasString(word.Lower()))
            {
                wxString replacement = GetString(word);
                text = text.Left(last) + replacement + text.Right(len - last - word.length());
                len = text.Length();
                i = last + replacement.Length();

                count ++;
            }
        }
    }

    output = text;

    return (count > 0);
}

// Do transformations, including replacing strings, and converting from HTML
bool wxTTSTransformer::TransformFile(const wxString& filename, const wxString& newFilename, int options)
{
    if (options & wxTTS_TRANSFORM_PASS_RAW_TEXT)
        options &= ~(wxTTS_TRANSFORM_OPTIONS_REMOVE_TITLE_ELEMENT|wxTTS_TRANSFORM_OPTIONS_REMOVE_XML_PREAMBLE|wxTTS_TRANSFORM_OPTIONS_SIMPLIFY_PUNCTUATION);

    // Optimize
    if (!HasStrings() && (options & wxTTS_TRANSFORM_PASS_RAW_TEXT))
    {
        wxCopyFile(filename, newFilename);
        return true;
    }

    wxTextFile textFile;
    if (textFile.Open(filename))
    {
        int changeCount = 0;
        size_t i;
        for (i = 0; i < textFile.GetLineCount(); i++)
        {
            bool changed = false;
            wxString line(textFile[i]), line2;
            if ((options & wxTTS_TRANSFORM_OPTIONS_WORD_SUBSTITUTION) && TransformString(line, line2))
            {
                line = line2;
                changed = true;
            }

            if (options & wxTTS_TRANSFORM_OPTIONS_REMOVE_TITLE_ELEMENT)
            {
                if (line.Find(wxT("<title>")) != wxNOT_FOUND || line.Find(wxT("<TITLE>")) != wxNOT_FOUND)
                {
                    line = wxEmptyString;
                    changed = true;
                }
            }

            if (options & wxTTS_TRANSFORM_OPTIONS_REMOVE_XML_PREAMBLE)
            {
                if (line.Find(wxT("<?xml")) != wxNOT_FOUND || line.Find(wxT("<!DOCTYPE")) != wxNOT_FOUND)
                {
                    line = wxEmptyString;
                    changed = true;
                }
            }

            if (options & wxTTS_TRANSFORM_OPTIONS_SIMPLIFY_PUNCTUATION)
            {
                if (ReplaceChar(line, (wxChar) 0x201C, wxT('"')))
                    changed = true;
                if (ReplaceChar(line, (wxChar) 0x201D, wxT('"')))
                    changed = true;
                if (ReplaceChar(line, (wxChar) 0x2013, wxT('-')))
                    changed = true;
                if (ReplaceChar(line, (wxChar) 0x2019, wxT('\'')))
                    changed = true;
                if (ReplaceChar(line, (wxChar) 0x2026, wxT('.')))
                    changed = true;
            }

            if (changed)
            {
                textFile[i] = line;
                changeCount ++;
            }
        }

        if (changeCount > 0)
        {
            {
                wxFileOutputStream stream(newFilename);
                wxTextOutputStream textStream(stream);
                size_t i;
                for (i = 0; i < textFile.GetLineCount(); i++)
                {
                    textStream << textFile[i] << wxT("\n");
                }
            }

            if (options & (wxTTS_TRANSFORM_HTML_TO_TEXT|wxTTS_TRANSFORM_HTML_TO_SAPI_XML|wxTTS_TRANSFORM_HTML_TO_SSML|wxTTS_TRANSFORM_TEXT_TO_SAPI_XML|wxTTS_TRANSFORM_TEXT_TO_SSML))
            {
                bool ok = false;
                wxString filename2(wxFileName::CreateTempFileName(wxT("speech")));
                {
                    wxFileOutputStream stream(filename2);
                    wxTextOutputStream textStream(stream);
                    
                    ok = ConvertFormat(newFilename, stream, textStream, options);
                }

                if (ok)
                {
                    wxRemoveFile(newFilename);
                    if (!wxRenameFile(filename2, newFilename))
                    {
                        wxCopyFile(filename2, newFilename);
                        wxRemoveFile(filename2);
                    }
                }
                else
                    wxRemoveFile(filename2);
            }
        }
        else
        {
            if (options & (wxTTS_TRANSFORM_HTML_TO_TEXT|wxTTS_TRANSFORM_HTML_TO_SAPI_XML|wxTTS_TRANSFORM_HTML_TO_SSML|wxTTS_TRANSFORM_TEXT_TO_SAPI_XML|wxTTS_TRANSFORM_TEXT_TO_SSML))
            {
                wxFileOutputStream stream(newFilename);
                wxTextOutputStream textStream(stream);
                return ConvertFormat(filename, stream, textStream, options);
            }
            else
            {
                wxCopyFile(filename, newFilename);
                return true;
            }
        }

        return true;
    }

    return false;
}

// Save the transformer to a file
bool wxTTSTransformer::SaveFile(const wxString& filename)
{
    wxFileOutputStream stream(filename);
    if (stream.IsOk())
    {
        wxTextOutputStream textStream(stream);
        size_t i;
        for (i = 0; i < m_strings.GetCount(); i++)
        {
            textStream << m_strings[i] << wxT(",");
            textStream << m_replacements[i] << wxT("\n");
        }

        m_modified = false;
        return true;
    }
    return false;
}

// Load the transformer from a file
bool wxTTSTransformer::LoadFile(const wxString& filename)
{
    Clear();
    wxTextFile file;
    if (file.Open(filename))
    {        
        size_t i;
        for (i = 0; i < file.GetLineCount(); i++)
        {
            wxString line = file.GetLine(i);
            wxString word = line.BeforeFirst(wxT(','));
            wxString replacement = line.AfterFirst(wxT(','));
            if (!word.IsEmpty() && !replacement.IsEmpty())
                AddString(word, replacement);
        }
        
        Index();
        
        m_modified = true;
        return true;
    }
    return false;
}

// Replace character with another character
bool wxTTSTransformer::ReplaceChar(wxString& str, const wxChar& ch1, const wxChar& ch2)
{
    wxString s1;
    s1 += ch1;
    wxString s2;
    s2 += ch2;
    return str.Replace(s1, s2) > 0;
}

// Convert from one format to another, e.g. text to XML, or HTML to text
bool wxTTSTransformer::ConvertFormat(const wxString& filename, wxOutputStream& rawStream, wxTextOutputStream& stream, int options)
{
    if (options & (wxTTS_TRANSFORM_HTML_TO_SAPI_XML|wxTTS_TRANSFORM_HTML_TO_SSML|wxTTS_TRANSFORM_HTML_TO_TEXT))
        return ConvertHTMLToText(filename, rawStream, stream, options);
    else if (options & wxTTS_TRANSFORM_TEXT_TO_SAPI_XML|wxTTS_TRANSFORM_HTML_TO_TEXT)
        return ConvertTextToXML(filename, rawStream, stream, options);
    else
        return false;
}

// Remove HTML tags from file, outputting text to stream
bool wxTTSTransformer::ConvertHTMLToText(const wxString& filename, wxOutputStream& WXUNUSED(rawStream), wxTextOutputStream& stream, int options)
{
    // TODO: should we ever write a BOM Unicode marker for text? SAPI doesn't use it
#if 0
    unsigned char buf[4];
    buf[0] = 0xEF;
    buf[1] = 0xBB;
    buf[2] = 0xBF;
    buf[3] = 0;
    rawStream.Write((const void*) buf, 3);
#endif

    // Mapping for entities that aren't spoken properly by SAPI
    wxArrayString entities;
    wxArrayString equivalents;

    // TODO: expand this, and separate it out
    entities.Add(wxT("copy"));
    equivalents.Add(wxT("copyright"));

    entities.Add(wxT("nbsp"));
    equivalents.Add(wxT(" "));

    if (options & wxTTS_TRANSFORM_HTML_TO_SAPI_XML)
    {
        stream << wxT("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<sapi>\n");
    }
    else if (options & wxTTS_TRANSFORM_HTML_TO_SSML)
    {
        stream << wxT("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n");
        stream << wxT("<speak xmlns=\"http://www.w3.org/2001/10/synthesis\"\
xmlns:dc=\"http://purl.org/dc/elements/1.1/\" version=\"1.0\">\n"); 

    }

    wxTTSSimpleHtmlParser parser;
    wxArrayString toReplace, replaceWith;

    wxHtmlEntitiesParser entitiesParser;
    
    if (parser.ParseFile(filename))
    {
        wxTTSSimpleHtmlTag* tag = parser.GetTopLevelTag()->GetChildren();

        int currentListLevel = -1;
        wxArrayInt      listLevels;
        wxArrayString   listTypes;
        wxString lastText;
        bool            suppressText = false;

        while (tag)
        {
            if ((tag->NameIs(wxT("ol")) || tag->NameIs(wxT("ul"))) && tag->GetType() == wxTTSSimpleHtmlTag_Open)
            {
                stream << wxT("\n");
                currentListLevel ++;
                if ((int) listLevels.GetCount() <= currentListLevel)
                {
                    listLevels.Add(0);
                    listTypes.Add(tag->GetName().Lower());
                }
                else
                {
                    listLevels[currentListLevel] = 0;
                    listTypes[currentListLevel] = tag->GetName().Lower();
                }

            }
            else if ((tag->NameIs(wxT("ol")) || tag->NameIs(wxT("ul"))) && tag->GetType() == wxTTSSimpleHtmlTag_Close)
            {
                currentListLevel --;
                if (currentListLevel < -1)
                    currentListLevel = -1;
            }
            else if (tag->NameIs(wxT("li")) && tag->GetType() == wxTTSSimpleHtmlTag_Open)
            {
                if (currentListLevel >= 0)
                {
                    listLevels[currentListLevel] ++;

                    if ((options & wxTTS_TRANSFORM_SPEAK_ORDERED_LIST) && currentListLevel >= 0 && listTypes[currentListLevel] == wxT("ol"))
                    {
                        stream << wxString::Format(wxT("%d. "), listLevels[currentListLevel]);
                    }
                }
            }
            else if (tag->NameIs(wxT("p")))
            {
                stream << wxT("\n");
            }
            else if (tag->NameIs(wxT("br")))
            {
                stream << wxT("\n");
            }
            else if (tag->NameIs(wxT("title")) /* && (options & wxTTS_TRANSFORM_OPTIONS_REMOVE_TITLE_ELEMENT) */)
            {
                if (tag->GetType() == wxTTSSimpleHtmlTag_Open)
                    suppressText = true;
                else
                    suppressText = false;
            }
            else if (tag->NameIs(wxT("h1")) || tag->NameIs(wxT("h2")) || tag->NameIs(wxT("h3")) || tag->NameIs(wxT("h4")) || tag->NameIs(wxT("h5")))
            {
                if (tag->GetType() == wxTTSSimpleHtmlTag_Open)
                {
                    stream << wxT("\n");
                    if (options & wxTTS_TRANSFORM_SPEAK_HEADINGS)
                    {
                        wxString headerNumberStr(tag->GetName().Mid(1));
                        int headingNumber = wxAtoi(headerNumberStr);
                        stream << wxT("Heading ") << headingNumber << wxT(": ");
                    }
                }
                else
                {
                    // Add punctuation to the end of a heading for correct intonation
                    if (lastText.Length() > 0)
                    {
                        wxChar ch = lastText.Last();
                        if (ch != wxT('.') && ch != wxT(',') && ch != wxT('!') && ch != wxT('?'))
                            stream << wxT(".");
                    }

                    stream << wxT("\n");
                }
            }
            else if (tag->GetType() == wxTTSSimpleHtmlTag_Text)
            {
                if (!suppressText)
                {
                    lastText = tag->GetText();
                    stream << lastText;
                }
            }
            else if (tag->GetType() == wxTTSSimpleHtmlTag_Entity)
            {
                if (!suppressText)
                {
                    wxChar ch = entitiesParser.GetEntityChar(tag->GetName());
                    if (ch != 0)
                    {
                        if (ch == 160) // nbsp
                            ch = wxT(' ');
                        
                        lastText = wxEmptyString;
                        lastText += ch;

                        // If we find an equivalent for the entity, use that
                        // instead, in either XML or text mode.

                        int pos = entities.Index(tag->GetName());
                        if (pos != wxNOT_FOUND)
                            lastText = equivalents[pos];
                        
                        if ((options & (wxTTS_TRANSFORM_HTML_TO_SAPI_XML|wxTTS_TRANSFORM_HTML_TO_SSML)) && pos == wxNOT_FOUND)
                        {
                            stream << wxT("&") << tag->GetName() << wxT(";");
                        }
                        else
                        {
                            stream << lastText;
                        }
                    }
                }
            }

            tag = tag->GetNext();
        }

        if (options & wxTTS_TRANSFORM_HTML_TO_SAPI_XML)
        {
            stream << wxT("\n</sapi>\n");
        }
        else if (options & wxTTS_TRANSFORM_HTML_TO_SSML)
        {
            stream << wxT("\n</speak>\n");
        }
    }
    else
        return false;

    return true;
}

// Convert plain text to SAPI XML
bool wxTTSTransformer::ConvertTextToXML(const wxString& filename, wxOutputStream& WXUNUSED(rawStream), wxTextOutputStream& stream, int options)
{
    if (options & wxTTS_TRANSFORM_TEXT_TO_SAPI_XML)
    {
        stream << wxT("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<sapi>\n");
    }
    else if (options & wxTTS_TRANSFORM_TEXT_TO_SSML)
    {
        stream << wxT("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n");
        stream << wxT("<speak xmlns=\"http://www.w3.org/2001/10/synthesis\"\
xmlns:dc=\"http://purl.org/dc/elements/1.1/\" version=\"1.0\">\n"); 

    }
    
    wxTextFile file;
    if (file.Open(filename))
    {        
        size_t i;
        for (i = 0; i < file.GetLineCount(); i++)
        {
            wxString line = file.GetLine(i);

            // TODO: convert Unicode to XML
            stream << line << wxT("\n");
        }
    }

    if (options & wxTTS_TRANSFORM_TEXT_TO_SAPI_XML)
    {
        stream << wxT("\n</sapi>\n");
    }
    else if (options & wxTTS_TRANSFORM_TEXT_TO_SSML)
    {
        stream << wxT("\n</speak>\n");
    }

    return true;
}

/*
 * wxTTSHandler implementation
 */

IMPLEMENT_CLASS( wxTTSHandler, wxEvtHandler )

wxTTSHandler::wxTTSHandler(const wxString& id, const wxString& displayName)
{
    Init();

    m_id = id;
    m_displayName = displayName;;
}

wxTTSHandler::~wxTTSHandler()
{
    DeleteTempFile();
    Uninitialize();
}

// Member initialisation
void wxTTSHandler::Init()
{
    m_options = 0;
    m_textToSpeech = NULL;
    m_isInitialized = false;
}

// Helpers
int wxTTSHandler::PercentToNativeValue(int value, int minValue, int maxValue)
{
    return (int) (minValue + (maxValue - minValue) * (double(value) / 100.0));
}

int wxTTSHandler::NativeValueToPercent(int value, int minValue, int maxValue)
{
    return (int) (100.0 * (value - minValue) / (maxValue - minValue)); 
}

// Set/get the speed
void wxTTSHandler::SetSpeed(int speed, wxTTSQuantityType quantity)
{
    if (quantity == wxTTS_PERCENT)
        speed = (int) PercentToNativeValue(speed, GetMinSpeed(), GetMaxSpeed());
    
    SetProperty(wxTTS_PROP_SPEED, speed);
}

int wxTTSHandler::GetSpeed(wxTTSQuantityType quantity) const
{
    int speed = (int) GetPropertyLong(wxTTS_PROP_SPEED);

    if (quantity == wxTTS_PERCENT)
        speed = NativeValueToPercent(speed, GetMinSpeed(), GetMaxSpeed()); 
    return speed;
}

// Set/get the volume
void wxTTSHandler::SetVolume(int vol, wxTTSQuantityType quantity)
{
    if (quantity == wxTTS_PERCENT)
        vol = (int) PercentToNativeValue(vol, GetMinVolume(), GetMaxVolume());
    
    SetProperty(wxTTS_PROP_VOLUME, vol);
}

int wxTTSHandler::GetVolume(wxTTSQuantityType quantity) const
{
    int vol = (int) GetPropertyLong(wxTTS_PROP_VOLUME);

    if (quantity == wxTTS_PERCENT)
        vol = NativeValueToPercent(vol, GetMinVolume(), GetMaxVolume());
    return vol;
}

// Set/get the pitch
void wxTTSHandler::SetPitch(int pitch, wxTTSQuantityType quantity)
{
    if (quantity == wxTTS_PERCENT)
        pitch = (int) PercentToNativeValue(pitch, GetMinPitch(), GetMaxPitch());
    SetProperty(wxTTS_PROP_PITCH, pitch);
}

int wxTTSHandler::GetPitch(wxTTSQuantityType quantity) const
{
    int pitch = (int) GetPropertyLong(wxTTS_PROP_PITCH);
    if (quantity == wxTTS_PERCENT)
        pitch = NativeValueToPercent(pitch, GetMinPitch(), GetMaxPitch());
    return pitch;
}

// Set a property
void wxTTSHandler::SetProperty(const wxTTSProperty& property)
{
    m_properties.SetProperty(property);
}

void wxTTSHandler::SetProperty(const wxString& name, const wxString& value)
{
    m_properties.SetProperty(name, value);
}

void wxTTSHandler::SetProperty(const wxString& name, const wxString& value, const wxArrayString& choices)
{
    m_properties.SetProperty(name, value);
    wxTTSProperty* prop = m_properties.FindProperty(name);
    prop->SetChoices(choices);
}

void wxTTSHandler::SetProperty(const wxString& name, long value)
{
    m_properties.SetProperty(name, value);
}

void wxTTSHandler::SetProperty(const wxString& name, int value)
{
    m_properties.SetProperty(name, (long) value);
}

void wxTTSHandler::SetProperty(const wxString& name, bool value)
{
    m_properties.SetProperty(name, value);
}

void wxTTSHandler::SetProperty(const wxVariant& value)
{
    m_properties.SetProperty(value.GetName(), value);
}

// Get a property
wxTTSProperty wxTTSHandler::GetProperty(const wxString& name) const
{
    wxTTSProperty* prop = m_properties.FindProperty(name);
    if (prop)
        return *prop;
    else
        return wxTTSProperty();
}

// Get a property value variant
wxVariant wxTTSHandler::GetPropertyValue(const wxString& name) const
{
    return m_properties.FindPropertyValue(name);
}

// Get a string property value
wxString wxTTSHandler::GetPropertyString(const wxString& name) const
{
    return m_properties.FindPropertyValueString(name);
}

// Get a long property value
long wxTTSHandler::GetPropertyLong(const wxString& name) const
{
    return m_properties.FindPropertyValueLong(name);
}

// Get a double property value
double wxTTSHandler::GetPropertyDouble(const wxString& name) const
{
    return m_properties.FindPropertyValueDouble(name);
}

// Get a bool property value
long wxTTSHandler::GetPropertyBool(const wxString& name) const
{
    return m_properties.FindPropertyValueBool(name);
}

// Does this property exist?
bool wxTTSHandler::HasProperty(const wxString& name) const
{
    return m_properties.FindProperty(name) != NULL;
}

// Get current properties - doesn't necessarily mean they're all supported
wxArrayString wxTTSHandler::EnumerateProperties() const
{
    wxArrayString props;
    size_t i;
    for (i = 0; i < m_properties.GetCount(); i++)
        props.Add(m_properties.GetProperty(i)->GetName());
    return props;
}

wxTTSProperty wxTTSHandler::GetProperty(size_t i) const
{
    wxTTSProperty* prop = m_properties.GetProperty(i);
    if (prop)
        return *prop;
    else
        return wxTTSProperty();
}

// Delete temporary file
void wxTTSHandler::DeleteTempFile()
{
    if (!m_tempFilename.IsEmpty())
    {
        wxRemoveFile(m_tempFilename);
        m_tempFilename.empty();
    }
}

// Create transformer options from handler's properties
int wxTTSHandler::CreateTransformerOptions(int mandatoryOptions) const
{
    int options = mandatoryOptions;

    if (GetPropertyBool(wxTTS_PROP_SIMPLIFY_PUNCTUATION))
        options |= wxTTS_TRANSFORM_OPTIONS_SIMPLIFY_PUNCTUATION;

    if (!GetPropertyBool(wxTTS_PROP_NO_SUBSTITUTIONS))
        options |= wxTTS_TRANSFORM_OPTIONS_WORD_SUBSTITUTION;

    return options;
}

// Do the transformation
wxString wxTTSHandler::DoTransformation(const wxString& originalFilename, int transformationFlags)
{
    wxTTSTransformer* transformer = GetTransformer();
    
    wxString filename(originalFilename);
    if (transformer && transformationFlags)
    {
        m_tempFilename = wxFileName::CreateTempFileName(wxT("speechinputfile"));
        
        transformer->TransformFile(originalFilename, m_tempFilename, transformationFlags);

        filename = m_tempFilename;
    }

    return filename;
}

// Get pronunciation transformer
wxTTSTransformer* wxTTSHandler::GetTransformer() const
{
    wxTTSTransformer* transformer = NULL;
    if (GetTextToSpeechObject())
        transformer = GetTextToSpeechObject()->GetTransformer();
    return transformer;
}

/*
 * wxTTSSpeechSettingsInfo
 */

/// Apply the settings to the text to speech object
bool wxTTSSpeechSettingsInfo::ApplySettings()
{
    if (!m_speech)
        return false;

    if (m_engine.IsEmpty() || !m_speech->SetEngine(m_engine))
        return false;

    m_speech->SetSpeed(m_speed, wxTTS_PERCENT);
    m_speech->SetVolume(m_volume, wxTTS_PERCENT);
    m_speech->SetPitch(m_pitch, wxTTS_PERCENT);

    return true;
}
